package se.sll.itintegration.catalogue.product.service;

import java.util.ArrayList;
import java.util.List;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.validation.Validator;
import javax.ws.rs.PathParam;
import lombok.extern.slf4j.Slf4j;
import se.sll.itintegration.catalogue.product.ejb.CodeEjb;
import se.sll.itintegration.catalogue.product.ejb.CodeSystemEjb;
import se.sll.itintegration.catalogue.product.entity.CodeEntity;
import se.sll.itintegration.catalogue.product.entity.CodeSystemEntity;
import se.sll.service.catalogue.common.exception.BusinessLogicException;
import se.sll.service.catalogue.common.exception.ResourceNotFoundException;
import se.sll.service.catalogue.common.exception.ServiceCatalogueException;
import se.sll.service.catalogue.common.exception.ServiceErrorCode;
import se.sll.service.catalogue.common.exception.ServiceException;
import se.sll.service.catalogue.common.exception.ValidationException;
import se.sll.service.catalogue.common.validation.FacadeRESTValidator;
import se.sll.service.catalogue.product.api.model.*;
import se.sll.service.catalogue.product.api.model.CodeResponse;

/**
 *
 * @author Jiri Uosukainen
 * @author Khaled Daham
 */
@Stateless
@Slf4j
public class CodeSystemService implements se.sll.service.catalogue.product.api.resource.CodeSystemService {

    @EJB
    private CodeSystemEjb codeSystemEjb;
    @EJB
    private CodeEjb codeEntityEjb;
    private static final String T_MEDIA_TYPE = "application/json; charset=UTF-8";
    @Inject
    private Validator validator;
    private FacadeRESTValidator facadeRESTValidator = new FacadeRESTValidator();

    // Services for code systems
    //***************************
    @Override
    public GetCodesystemsResponse getCodesystems() throws ServiceException {
        log.debug("CodeSystemFacadeREST.list() was called");
        return GetCodesystemsResponse.jsonOK(returnCodeSystem(codeSystemEjb.findAll()));
    }

    @Override
    public GetCodesystemsByCodeSystemResponse getCodesystemsByCodeSystem(
            @PathParam("codeSystem") String codeSystem) throws ServiceException {
        log.debug("CodeSystemFacadeREST.read() was called");

        CodeSystemEntity cse = null;
        if (codeSystem != null) {
            try {
                List<CodeSystemEntity> cseL = codeSystemEjb.findByOid(codeSystem);
                if (cseL != null && cseL.size() == 1) {
                    cse = cseL.get(0);
                } else {
                    throw new ResourceNotFoundException(ServiceErrorCode.CODE_SYSTEM_NOT_FOUND_OID, codeSystem);
                }
            } catch (RuntimeException e) {
                log.error("CodeSystemFacadeREST.read Caught RuntimeException: {}", new Object[]{e.getMessage()});
                throw new ServiceCatalogueException();
            }
        } else {
            log.debug("Codesystem not found, codeSystem:" + codeSystem);
            throw new ResourceNotFoundException(ServiceErrorCode.CODE_SYSTEM_NOT_FOUND_OID, codeSystem);
        }
        return GetCodesystemsByCodeSystemResponse.jsonOK(returnCodeSystem(cse));
    }

    /**
     *
     * @param entity
     * @throws ServiceException
     */
    @Override
    public PostCodesystemsResponse postCodesystems(CodeSystemCreate entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.create() was called");
        log.debug(entity.toString());

        facadeRESTValidator.validate(entity, validator);

        List<CodeSystemEntity> existingPs = codeSystemEjb.findByOid(entity.getOid());

        if (existingPs.isEmpty()) { // OK to insert
            try {
                CodeSystemEntity cse = new CodeSystemEntity();
                cse.setName(entity.getName());
                cse.setDescription(entity.getDescription());
                cse.setOid(entity.getOid());
                codeSystemEjb.create(cse);
            } catch (RuntimeException e) {
                log.error("CodeSystemFacadeREST.create() Caught RuntimeException: " + e.getMessage());
                throw new ServiceCatalogueException();
            }
        } else {
            throw new ServiceCatalogueException(ServiceErrorCode.CODE_SYSTEM_ALREADY_EXISTS_OID, entity);
        }

        return PostCodesystemsResponse.created();
    }

    @Override
    public PutCodesystemsResponse putCodesystems(CodeSystemCreate entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.update() was called");
        log.debug(entity.toString());

        facadeRESTValidator.validate(entity, validator);

        List<CodeSystemEntity> existingPs = codeSystemEjb.findByOid(entity.getOid());
        if (existingPs.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.CODE_SYSTEM_NOT_FOUND_OID, entity.getOid());
        }
        if (existingPs.size() == 1) { // OK to insert
            try {
                CodeSystemEntity cse = existingPs.get(0);
                cse.setName(entity.getName());
                cse.setDescription(entity.getDescription());
                codeSystemEjb.edit(cse);
            } catch (RuntimeException e) {
                log.error("CodeSystemFacadeREST.update() Caught RuntimeException: " + e.getMessage());
                throw new ServiceCatalogueException();
            }
        } else {
            log.error("Codesystem not unique, codeSystem: " + entity);
            throw new ServiceCatalogueException(ServiceErrorCode.CODE_SYSTEM_ALREADY_EXISTS_OID, entity);
        }

        return PutCodesystemsResponse.oK();
    }

    @Override
    public DeleteCodesystemsByCodeSystemResponse deleteCodesystemsByCodeSystem(
            @PathParam("codeSystem") String codeSystem, CodeSystemDelete entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.delete() was called");

        facadeRESTValidator.validate(codeSystem, validator);

        List<CodeSystemEntity> cseL = codeSystemEjb.findByOid(entity.getOid());
        if (cseL == null || cseL.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.CODE_SYSTEM_NOT_FOUND_OID, entity.getOid());
        } else if (cseL.size() == 1) {
            codeSystemEjb.remove(cseL.get(0));
            return DeleteCodesystemsByCodeSystemResponse.oK();
        } else {
            log.error("Codesystem not unique, codeSystem: " + codeSystem);
            throw new ServiceCatalogueException(ServiceErrorCode.CODE_SYSTEM_ALREADY_EXISTS_OID, codeSystem);
        }
    }

    // Services for code
    //*******************
    /**
     * List all codes in a given code system
     *
     * @return All codes in the specified code system
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    @Override
    public GetCodesystemsCodesResponse getCodesystemsCodes() throws ServiceException {
        return GetCodesystemsCodesResponse.jsonOK(returnCodes(codeEntityEjb.findAll()));
    }

    @Override
    public GetCodesystemsByCodeSystemCodesResponse getCodesystemsByCodeSystemCodes(
            @PathParam("codeSystem") String codeSystem) throws ServiceException {
        log.debug("listCodes: " + codeSystem);
        List<Code> codeList = returnCodeList(codeEntityEjb.getCodeListByCodeSystem(codeSystem));

        return GetCodesystemsByCodeSystemCodesResponse.jsonOK(codeList);
    }

    /**
     * /codesystem/oid/ (object)
     *
     * @param codeSystem
     * @param entity
     * @throws ServiceException
     */
    @Override
    public PostCodesystemsByCodeSystemCodesResponse postCodesystemsByCodeSystemCodes(
            @PathParam("codeSystem") String codeSystem, CodeCreate entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.createCode() was called");
        log.debug("CodeSystem: " + codeSystem);
        log.debug(entity.toString());

        facadeRESTValidator.validate(entity, validator);

        List<CodeSystemEntity> existingCs = codeSystemEjb.findByOid(codeSystem);
        if (existingCs == null || existingCs.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.CODE_SYSTEM_NOT_FOUND_OID, codeSystem);
        }

        CodeEntity existingCe;
        try {
            existingCe = codeEntityEjb.findUniqueCode(codeSystem, entity.getCode());
        } catch (BusinessLogicException e) {
            throw new ServiceCatalogueException();
        }

        if (existingCs.size() == 1) { // Unique codesystem
            if (existingCe == null) { // A new code
                log.debug("Found : " + existingCs.toString());

                CodeEntity ce = new CodeEntity();
                ce.setCode(entity.getCode());
                ce.setDescription(entity.getDescription());
                ce.setDisplayName(entity.getDisplayName());
                ce.setCodeSystem(existingCs.get(0));

                try {
                    codeSystemEjb.create(ce);
                } catch (RuntimeException e) {
                    log.error("CodeSystemFacadeREST.createCode() Caught RuntimeException: ", e);
                    throw new ServiceCatalogueException();
                }
            } else {
                String msg = "Code already exists: code = " + entity;
                log.error(msg);
                throw new ServiceCatalogueException(ServiceErrorCode.CODE_ALREADY_EXISTS_CODESYSTEM_CODE, codeSystem, entity);
            }
        } else {
            log.error("CodeSystem not unique, codeSystem: " + codeSystem);
            throw new ServiceCatalogueException(ServiceErrorCode.CODE_SYSTEM_ALREADY_EXISTS_OID, codeSystem);
        }

        return PostCodesystemsByCodeSystemCodesResponse.created();
    }

    @Override
    public PutCodesystemsByCodeSystemCodesResponse putCodesystemsByCodeSystemCodes(
            @PathParam("codeSystem") String codeSystem, CodeCreate entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.updateCode() was called");
        log.debug(codeSystem);
        log.debug(entity.toString());

        facadeRESTValidator.validate(entity, validator);

        CodeEntity ce;
        try {
            ce = codeEntityEjb.findUniqueCode(codeSystem, entity.getCode());
        } catch (BusinessLogicException e) {
            log.error("CodeSystemFacadeREST.updateCode() Caught BusinessLogicException: ", e);
            throw new ServiceCatalogueException();
        }
        if (ce != null) {
            try {
                ce.setDescription(entity.getDescription());
                ce.setDisplayName(entity.getDisplayName());
                codeEntityEjb.edit(ce);
            } catch (RuntimeException e) {
                log.error("CodeSystemFacadeREST.updateCode() Caught RuntimeException: ", e);
                throw new ServiceCatalogueException();
            }
        } else {
            throw new ServiceCatalogueException();
        }
        return PutCodesystemsByCodeSystemCodesResponse.oK();
    }

    @Override
    public DeleteCodesystemsByCodeSystemCodesResponse deleteCodesystemsByCodeSystemCodes(
            @PathParam("codeSystem") String codeSystem, CodeDelete entity) throws ServiceException {
        log.debug("CodeSystemFacadeREST.readCode() was called");

        facadeRESTValidator.validate(entity, validator);

        CodeEntity ce = null;
        try {
            ce = codeEntityEjb.findUniqueCode(codeSystem, entity.getCode());
        } catch (RuntimeException e) {
            throw new ServiceCatalogueException();
        } catch (BusinessLogicException e) {
            log.error("CodeSystemFacadeREST.deleteCode() Caught BusinessLogicException: ", e);
            throw new ServiceCatalogueException();
        }

        if (ce != null) {
            codeEntityEjb.remove(ce);
        } else {
            throw new ServiceCatalogueException();
        }

        return DeleteCodesystemsByCodeSystemCodesResponse.oK();
    }

    /**
     * Find codes by codeSystem and codeName, codeName will be used with
     * wildcards
     *
     * @param codeSystem
     * @param name
     * @return
     * @throws ServiceException
     */
    @Override
    public GetCodesystemsByCodeSystemCodesByNameResponse getCodesystemsByCodeSystemCodesByName(
            @PathParam("name") String name, @PathParam("codeSystem") String codeSystem) throws ServiceException {
        log.debug("CodeSystemFacadeREST.readCode() was called");
        log.debug("Input: [codeSystem={}, codeName={}]", new Object[]{codeSystem, name});
        try {
            CodeResponse code = returnCode(codeEntityEjb.findUniqueCode(codeSystem, name), codeSystem);
            return GetCodesystemsByCodeSystemCodesByNameResponse.jsonOK(code);
        } catch (BusinessLogicException e) {
            log.error("readCode", e);
            throw new ServiceCatalogueException();
        }
    }

    /**
     * Find codes by name, regardless of code system
     *
     * @param name
     * @return All codes with the given name
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    @Override
    public GetCodesystemsCodesByNameResponse getCodesystemsCodesByName(@PathParam("name") String name) throws ServiceException {
        log.debug("CodeSystemFacadeREST.findCodeByName() was called");
        return GetCodesystemsCodesByNameResponse.jsonOK(returnCodes(codeEntityEjb.findByName(name)));
    }

    /**
     *
     * @param displayName
     * @return
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    @Override
    public GetCodesystemsCodesDisplaynamecontainsByDisplayNameResponse getCodesystemsCodesDisplaynamecontainsByDisplayName(
            @PathParam("displayName") String displayName) throws ServiceException {
        log.debug("CodeSystemFacadeREST.findCodesByDisplayNameAndByNoCodeSystem() was called");
        if (displayName == null || "".equals(displayName)) {
            String msg = String.format("Validation failed, displayName: %s", displayName);
            log.info("CodeSystemFacadeREST.findCodesByDisplayNameAndByNoCodeSystem(): {}", msg);
            throw new ValidationException(ServiceErrorCode.INVALID_DISPLAY_NAME_EMPTY);
        }
        return GetCodesystemsCodesDisplaynamecontainsByDisplayNameResponse.jsonOK(
                this.returnCodes(codeEntityEjb.findByDisplayName(displayName)));
    }

    /**
     *
     * @return
     */
    @Override
    public GetCodesystemsCodesDescriptioncontainsByDescriptionResponse getCodesystemsCodesDescriptioncontainsByDescription(
            @PathParam("description") String description) throws ServiceException {
        log.debug("CodeSystemFacadeREST.findCodesByDescriptionAndByNoCodeSystem() was called");
        if (description == null || "".equals(description)) {
            String msg = String.format("Validation failed, description: %s", description);
            log.info("CodeSystemFacadeREST.findCodesByDescriptionAndByNoCodeSystem(): {}", msg);
            throw new ValidationException(ServiceErrorCode.INVALID_DESCRIPTION_EMPTY);

        }
        return GetCodesystemsCodesDescriptioncontainsByDescriptionResponse.jsonOK(
                this.returnCodes(codeEntityEjb.findByDescription(description)));
    }

    /**
     *
     * @param ce
     * @param oid
     * @return
     */
    private CodeResponse returnCode(CodeEntity ce, String oid) {
        return new CodeResponse()
                .withCode(ce.getCode())
                    .withDescription(ce.getDescription())
                .withDisplayName(ce.getDisplayName())
                    .withOid(oid);
    }

    /**
     *
     * @param codeEntityList
     * @return
     */
    private List<Code> returnCodes(List<CodeEntity> codeEntityList) {
        List<Code> responseList = new ArrayList<>();
        for (CodeEntity ce : codeEntityList) {
            Code cr = new Code();
            cr.setCode(ce.getCode());
            cr.setDescription(ce.getDescription());
            cr.setDisplayName(ce.getDisplayName());
            cr.setOid(ce.getCodeSystem().getOid());
            responseList.add(cr);
        }
        return responseList;
    }

    private CodeSystemList returnCodeSystem(CodeSystemEntity cse) {
        CodeSystemList csr = new CodeSystemList();
        csr.setDescription(cse.getDescription());
        csr.setName(cse.getName());
        csr.setOid(cse.getOid());
        for (CodeEntity c : cse.getCodes()) {
            csr.getCodes().add(new Code()
                            .withCode(c.getCode())
                            .withCodeSystem(c.getCodeSystem().getName())
                            .withDescription(c.getDescription())
                            .withDisplayName(c.getDisplayName())
            );
        }
        return csr;
    }

    /**
     *
     * @param cseList
     * @return
     */
    private List<CodeSystemList> returnCodeSystem(List<CodeSystemEntity> cseList) {
        List<CodeSystemList> responseList = new ArrayList<>();
        for (CodeSystemEntity cse : cseList) {
            CodeSystemList csr = new CodeSystemList();
            csr.setDescription(cse.getDescription());
            csr.setName(cse.getName());
            csr.setOid(cse.getOid());
            csr.setCodes(new ArrayList<Code>());
            for (CodeEntity c : cse.getCodes()) {
                Code csc = new Code();
                csc.setCode(c.getCode());
                csc.setDescription(c.getDescription());
                csc.setDisplayName(c.getDisplayName());
                csc.setOid(csr.getOid());
                csr.getCodes().add(csc);
            }
            responseList.add(csr);
        }

        return responseList;
    }

    private List<Code> returnCodeList(List<se.sll.service.catalogue.common.rest.response.CodeResponse> cseList) {
        List<Code> responseList = new ArrayList<>();
        for (se.sll.service.catalogue.common.rest.response.CodeResponse cse : cseList) {
            Code csr = new Code();
            csr.setDescription(cse.getDescription());
            csr.setDisplayName(cse.getDisplayName());
            csr.setOid(cse.getOid());
            responseList.add(csr);
        }
        return responseList;
    }
}
